package it.tukano.swingdeco.containers;

import it.tukano.fun.Function1;
import it.tukano.fun.Function2;
import it.tukano.fun.Tuple2;
import it.tukano.fun.VFunction3;
import it.tukano.swingdeco.containers.ResizablePanel.Edge;
import it.tukano.swingdeco.draganddrop.DndUtilities;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import javax.swing.JPanel;
import javax.swing.JTabbedPane;

/**
 * A panel split in five resizable regions, each with a JTabbedPane
 * @author pgi
 */
public class RegionPanel extends JPanel {

    private static final long serialVersionUID = 0;

    private final ResizablePanel north = new ResizablePanel(Edge.BOTTOM);
    private final ResizablePanel east = new ResizablePanel(Edge.LEFT);
    private final ResizablePanel south = new ResizablePanel(Edge.TOP);
    private final ResizablePanel west = new ResizablePanel(Edge.RIGHT);
    private final JPanel center = new JPanel(new BorderLayout());
    private final Map<String, JTabbedPane> tabs = new HashMap<String, JTabbedPane>();
    private final Map<String, ResizablePanel> displays = new HashMap<String, ResizablePanel>();
    private final Map<Component, JTabbedPane> componentToLastTabMap = new HashMap<Component, JTabbedPane>();

    /**
     * Initializes this region panel
     */
    public RegionPanel() {
        super(new BorderLayout());
        tabs.put(BorderLayout.NORTH, new JTabbedPane());
        tabs.put(BorderLayout.EAST, new JTabbedPane());
        tabs.put(BorderLayout.CENTER, new JTabbedPane());
        tabs.put(BorderLayout.WEST, new JTabbedPane());
        tabs.put(BorderLayout.SOUTH, new JTabbedPane());
        displays.put(BorderLayout.NORTH, north);
        displays.put(BorderLayout.EAST, east);
        displays.put(BorderLayout.SOUTH, south);
        displays.put(BorderLayout.WEST, west);
        north.setView(tabs.get(BorderLayout.NORTH));
        east.setView(tabs.get(BorderLayout.EAST));
        west.setView(tabs.get(BorderLayout.WEST));
        center.add(tabs.get(BorderLayout.CENTER));
        south.setView(tabs.get(BorderLayout.SOUTH));
        add(north, BorderLayout.NORTH);
        add(west, BorderLayout.WEST);
        add(center, BorderLayout.CENTER);
        add(south, BorderLayout.SOUTH);
        add(east, BorderLayout.EAST);
    }

    public RegionPanel enableTabsDragAndDrop() {
        enableTabsDragAndDrop(null);
        return this;
    }

    /**
     * Installs the dnd utilities required to handle the drag and drop of the tabs.
     * @param dragFilter the function that will approve or reject the drag of a specific component. Use this if
     * you want to avoid the dnd of some tab
     * @return this
     */
    public RegionPanel enableTabsDragAndDrop(final Function1<Component, Boolean> dragFilter) {
        for(final JTabbedPane tab : tabs.values()) {
            DndUtilities.installLocalObjectDragHandler(tab, Integer.class, new Function2<Component, Point, Integer>() {

                public Integer apply(Component a, Point b) {
                    JTabbedPane pane = (JTabbedPane) a;
                    int selectedIndex = pane.getSelectedIndex();
                    if(selectedIndex >= 0) {
                        Rectangle bounds = pane.getBoundsAt(selectedIndex);
                        if(bounds.contains(b)) {
                            Component component = pane.getComponentAt(selectedIndex);
                            if(dragFilter == null) return component.hashCode();
                            if(dragFilter.apply(component)) return component.hashCode();
                        }
                    }
                    return null;
                }
            });
            DndUtilities.installDropHandler(tab, Arrays.<Class<?>>asList(Integer.class), new VFunction3<Component, Point, Map<Class<?>, Object>>() {

                @Override
                protected void doApply(Component p1, Point p2, Map<Class<?>, Object> p3) {
                    Integer componentHashCode = (Integer) p3.get(Integer.class);
                    if(componentHashCode != null) {
                        Tuple2<JTabbedPane, Component> source = RegionPanel.this.getTabForComponent(componentHashCode);
                        if(source != null && source.getA() != tab) {
                            RegionPanel.this.move(source.getB(), source.getA(), tab);
                        }                        
                    }
                }
            });
        }
        return this;
    }
    
    private String regionForTab(JTabbedPane tab, String defaultValue) {
        for (Map.Entry<String, JTabbedPane> entry : tabs.entrySet()) {
            if(entry.getValue() == tab) {
                defaultValue = entry.getKey();
                break;
            }
        }
        return defaultValue;
    }

    public void display(Component c, String region, boolean useLastTab) {
        if(useLastTab) {
            region = regionForTab(componentToLastTabMap.get(c), region);
        }
        display(c, region);
    }

    public RegionPanel display(Component c, String region) {
        JTabbedPane tab = tabs.get(region);
        componentToLastTabMap.put(c, tab);
        boolean empty = tab.getTabCount() == 0;
        int index = tab.indexOfComponent(c);
        if(index < 0) {
            tab.addTab(c.getName(), c);
            tab.setSelectedIndex(tab.indexOfComponent(c));
            tab.setVisible(true);
            if(empty) {
                ResizablePanel display = displays.get(region);
                if(display != null) display.pack();
            }
        } else {
            tab.setSelectedIndex(index);
        }
        return this;
    }

    public void hide(Component component) {
        for (JTabbedPane tab : tabs.values()) {
            int index = tab.indexOfComponent(component);
            if(index >= 0) {
                tab.remove(index);
                if(tab.getTabCount() == 0) {
                    tab.setVisible(false);
                    ResizablePanel display = displayForTab(tab);
                    if(display != null) {
                        display.pack();
                    }
                }
                break;
            }
        }
    }

    private ResizablePanel displayForTab(JTabbedPane tab) {
        for (ResizablePanel panel : displays.values()) {
            if(panel.hasView(tab)) return panel;
        }
        return null;
    }

    /** Finds the tab that contains the given component, if any */
    private Tuple2<JTabbedPane, Component> getTabForComponent(Integer componentHashCode) {
        for (JTabbedPane jTabbedPane : tabs.values()) {
            for (Component component : jTabbedPane.getComponents()) {
                if(component.hashCode() == componentHashCode) return Tuple2.newInstance(jTabbedPane, component);
            }
        }
        return null;
    }

    /** Moves a tab from a region to another one */
    private void move(Component p, JTabbedPane source, JTabbedPane tab) {
        source.remove(p);
        tab.addTab(p.getName(), p);
        componentToLastTabMap.put(p, tab);
        tab.setSelectedComponent(p);
    }

    public void updateTabName(Component component) {
        Tuple2<JTabbedPane, Component> tabForComponent = getTabForComponent(component.hashCode());
        if(tabForComponent != null) {
            JTabbedPane tabbedPane = tabForComponent.getA();
            int index = tabbedPane.indexOfComponent(component);
            if(index >= 0) {
                tabbedPane.setTitleAt(index, component.getName());
            }
        }
    }

    public void adaptRegionToPreferredSize(Component panel) {
        Tuple2<JTabbedPane, Component> tabForComponent = getTabForComponent(panel.hashCode());
        if(tabForComponent != null) {
            JTabbedPane tab = tabForComponent.getA();
            String region = regionForTab(tabForComponent.getA(), null);
            if(region != null) {
                Dimension size = tab.getPreferredSize();
                ResizablePanel rp = displays.get(region);
                rp.pack();
            }
        }
    }
}
